/*
 *  Copyright 2004 Blandware (http://www.blandware.com)
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.blandware.atleap.webapp.servlet;

import com.blandware.atleap.common.Constants;
import com.blandware.atleap.common.util.ConvertUtil;
import com.blandware.atleap.model.core.ContentDocument;
import com.blandware.atleap.model.core.ContentResource;
import com.blandware.atleap.model.core.Role;
import com.blandware.atleap.service.core.ContentResourceManager;
import com.blandware.atleap.webapp.util.core.CacheUtil;
import com.blandware.atleap.webapp.util.core.GlobalProperties;
import com.blandware.atleap.webapp.util.core.WebappConstants;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;

/**
 * <p>Servlet to return content resource</p>
 * <p>Following attributes are accepted:
 * <ul>
 * <li>
 * <code>uri</code> -- the URI of resource to obtain (can be passed as a request
 * parameter or request attribute)
 * </li>
 * <li>
 * <code>view</code> -- whether the resource will be just viewed. This has a
 * higher precedence than <code>download</code> (can be passed as a request
 * parameter)
 * </li>
 * <li>
 * <code>download</code> -- whether the resource will be downloaded (can be
 * passed as a request parameter)
 * </li>
 * </ul>
 * </p>
 * <p><a href="ResourceServlet.java.html"><i>View Source</i></a></p>
 *
 * @author Andrey Grebnev <a href="mailto:andrey.grebnev@blandware.com">&lt;andrey.grebnev@blandware.com&gt;</a>
 * @version $Revision: 1.4 $ $Date: 2007/11/30 13:01:58 $
 * @web.servlet name="resource" load-on-startup="1"
 * @web.servlet-mapping url-pattern="/servlet/resource/*"
 */
public class ResourceServlet extends HttpServlet {

	/**
	 * Commons Logging instance.
	 */
	protected transient final Log log = LogFactory.getLog(ResourceServlet.class);

	/**
	 * Initializes the servlet.
	 */
	public void init(ServletConfig config) throws ServletException {
		super.init(config);
	}

	/**
	 * Destroys the servlet.
	 */
	public void destroy() {
        super.destroy();
	}

	/**
	 * Processes requests for both HTTP <code>GET</code> and <code>POST</code> methods.
	 *
	 * @param request  servlet request
	 * @param response servlet response
	 */
	protected void processRequest(HttpServletRequest request, HttpServletResponse response)
	        throws ServletException, IOException {
		ApplicationContext ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext());

		String uri = null;
		if ( request.getParameter("uri") != null ) {
			uri = request.getParameter("uri");
		} else if ( request.getAttribute("uri") != null ) {
			uri = (String) request.getAttribute("uri");
		} else {
			String message = "Missing 'uri' parameter in the request";
			if ( log.isWarnEnabled() ) {
				log.warn(message);
			}
			response.sendError(HttpServletResponse.SC_NOT_FOUND);
			return;
		}

		log.debug("URI: " + uri);

		boolean download = request.getParameter("download") != null;
		boolean view = request.getParameter("view") != null;

		byte[] resourceData = null;
		String mimeType = null;
		List roleNames = new ArrayList();
		String charset = null;
		long lastModified = -1;

		//try to get from cache
		CacheUtil cacheUtil = CacheUtil.getInstance(request);
		CacheUtil.ResourceData rd = cacheUtil.getResourceFromCache(uri);

		if ( rd != null ) {
			resourceData = rd.getData();
            //WARNING the resource data can be null if its size is big. We need it in order to allow return SC_NOT_MODIFIED
			mimeType = rd.getMimeType();
			roleNames = ConvertUtil.convertStringToList(rd.getRoles(), ",", true);
			charset = rd.getCharset();
			lastModified = rd.getLastModified();

			if ( log.isDebugEnabled() ) {
				log.debug("Retrieving resource from Cache with mimeType=" + mimeType + " and uri=" + uri + " and roles=" + roleNames.toString());
			}
		}

		//check ifnonmatch header
		if ( request.getMethod().equals("GET") && lastModified != -1 ) {
            if (processBrowserCache(roleNames, lastModified, request, response))
                return;
		}


		if ( resourceData == null ) {
			//get from DB
			ContentResourceManager contentResourceManager = (ContentResourceManager) ctx.getBean(Constants.CONTENT_RESOURCE_MANAGER_BEAN);
			ContentResource contentResource = contentResourceManager.findContentResourceByUri(uri);
			if ( contentResource == null ) {
				if ( log.isWarnEnabled() ) {
					log.warn("No content resource with uri=" + uri + " could be found");
				}
				response.sendError(HttpServletResponse.SC_NOT_FOUND);
				return;
			}
			resourceData = contentResource.getResourceData().getData();
			mimeType = contentResource.getMimeType();
			lastModified = contentResource.getLastUpdatedDatetime().getTime();

			if ( contentResource instanceof ContentDocument ) {
				charset = ((ContentDocument) contentResource).getCharset();
			}

			//getting names of roles
			List roles = contentResource.getRoles();
			for ( int i = 0; i < roles.size(); i++ ) {
				Role role = (Role) roles.get(i);
				roleNames.add(role.getName());
			}

			//put into cache
			String roleNamesString = null;
			if ( roleNames.size() > 0 ) {
				roleNamesString = ConvertUtil.convertListToString(roleNames, ",");
			}
			rd = new CacheUtil.ResourceData(resourceData, mimeType, charset, roleNamesString, lastModified);
			cacheUtil.putResourceInCache(rd, uri);
			if ( log.isDebugEnabled() ) {
				log.debug("Retrieving resource from DB with mimeType=" + mimeType + " and uri=" + uri);
			}
		}

        if (processBrowserCache(roleNames, lastModified, request, response))
            return;

		//check roles
		boolean granted = false;
		if ( roleNames != null && roleNames.size() > 0 ) {
			for ( int i = 0; i < roleNames.size(); i++ ) {
				String roleName = (String) roleNames.get(i);
				if ( request.isUserInRole(roleName) ) {
					if ( log.isDebugEnabled() ) {
						log.debug(" User '" + request.getRemoteUser() +
						        "' has role '" + roleName + "', granting access");
					}
					granted = true;
					break;
				}
			}
		} else {
			granted = true;
			if ( log.isDebugEnabled() ) {
				log.debug("Anonymous user granting access");
			}
		}

		if ( !granted ) {
			// The current user is not authorized for this action
			if ( log.isDebugEnabled() ) {
				log.debug(" User '" + request.getRemoteUser() +
				        "' does not have any required role, denying access");
			}
			response.sendError(HttpServletResponse.SC_FORBIDDEN);
			return;
		}

		String charsetSuffix = "";
		if ( charset != null ) {
			charsetSuffix = ";charset=" + charset;
		}

		if ( !download && !view && mimeType != null ) {
			response.setContentType(mimeType + charsetSuffix);
		} else {
			String filename = "";
			int slash = uri.lastIndexOf("/");
			if ( slash >= 0 ) {
				filename = uri.substring(slash + 1, uri.length());
			}
			if ( (view || (!view && !download)) && mimeType != null ) {
				response.setHeader("Content-Disposition", "inline; filename=\"" + filename + "\"");
				response.setContentType(mimeType);
				response.setContentType(mimeType + charsetSuffix);
			} else {
				response.setHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
				response.setContentType("application/octet-stream");
			}
			response.setHeader("Content-Transfer-Encoding", "binary");
		}

		OutputStream out = response.getOutputStream();
		response.setContentLength(resourceData.length);
		out.write(resourceData);
		out.flush();
		out.close();
	}

    /**
     * Sets up cache headers
     *
     * @param roleNames List of roles assigned to resource
     * @param request   The request being processed
     * @param response  The response being generated
     * @return <code>true</code> if we should return SC_NOT_MODIFIED
     */
    protected boolean processBrowserCache(List roleNames, long lastModified, HttpServletRequest request, HttpServletResponse response) {
        //ETag generation
        String username = request.getRemoteUser();
        StringBuffer eTagBuffer = new StringBuffer("W/\"").append(username).append("-").append(lastModified).append("\"");
        String eTag = eTagBuffer.toString();
        response.setHeader("ETag", eTag);

        boolean conditionSatisfied = false;

        int maxAge = ((Integer)GlobalProperties.getInstance(getServletContext()).getInteger(WebappConstants.CACHE_RESOURCE_MAXAGE_PROPERTY, -1)).intValue();
        if ( maxAge < 0 ) {
            response.setHeader("Cache-Control", "no-cache,no-store,max-age=0");
            response.setHeader("Pragma", "no-cache");
        } else {
            String cacheType = "public";
            if (roleNames.size() != 0) {
                cacheType = "private";
            }
            response.setHeader("Cache-Control", cacheType + ",max-age="+maxAge);
            response.setHeader("Pragma", "");

            String headerValue = request.getHeader("If-None-Match");
            if (headerValue != null) {
                if (!headerValue.equals("*")) {
                    StringTokenizer commaTokenizer =
                            new StringTokenizer(headerValue, ",");
                    while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) {
                        String currentToken = commaTokenizer.nextToken();
                        if (currentToken.trim().equals(eTag)) {
                            conditionSatisfied = true;
                        }
                    }
                } else {
                    conditionSatisfied = true;
                }
                if (conditionSatisfied) {
                    response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                }
            }
        }

        return conditionSatisfied;

    }

	/**
	 * Handles the HTTP <code>GET</code> method.
	 *
	 * @param request  servlet request
	 * @param response servlet response
	 */
	protected void doGet(HttpServletRequest request, HttpServletResponse response)
	        throws ServletException, IOException {
		processRequest(request, response);
	}

	/**
	 * Handles the HTTP <code>POST</code> method.
	 *
	 * @param request  servlet request
	 * @param response servlet response
	 */
	protected void doPost(HttpServletRequest request, HttpServletResponse response)
	        throws ServletException, IOException {
		processRequest(request, response);
	}

	/**
	 * Returns a short description of the servlet.
	 */
	public String getServletInfo() {
		return "Servlet to return content resource";
	}

}